/**
 * Copyright 2013 Douglas Lacher
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * @author Douglas Lacher <doug.lacher@gmail.com>
 */

package com.devioustoast.andmoco;

import java.io.IOException;
import java.io.OutputStream;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;

import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothSocket;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.util.Log;

/**
 * Represents a Bluetooth connection
 *
 * @author Douglas Lacher <doug.lacher@gmail.com>
 */
public class BluetoothConnector extends Thread {

  public static final String LOG_TAG = "BluetoothConnector";
  public static final int REQUEST_ENABLE_BT = 1;
  public static final UUID SPP_UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB");
  public static final String ARCBOTICS_BLUETOOTH_NAME = "ArcBotics";
  public static final String COMMAND_TERMINATOR = "\r";

  public interface ConnectionStateChangeListener {
    public void onConnectionStateChange(ConnectionState newState);
  }

  public enum ConnectionState {
    CONNECTED,
    CONNECTING,
    DISCONNECTED,
  }

  private static boolean mConnectLock;
  private static boolean mSendLock;
  private static boolean mRun;
  private static BlockingQueue<String> mSendQueue = new ArrayBlockingQueue<String>(50, true);

  private BluetoothAdapter mBluetoothAdapter = null;
  private BluetoothDevice mBluetoothDevice = null;
  private BluetoothSocket mBluetoothSocket = null;
  private ConnectionStateChangeListener mListener = null;

  public final BroadcastReceiver receiver = new BroadcastReceiver() {
    public void onReceive(Context context, Intent intent) {
      Log.d(LOG_TAG, "Broadcast received.");
      String action = intent.getAction();
      if (BluetoothDevice.ACTION_FOUND.equals(action)) {
        BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
        Log.v(LOG_TAG, String.format("Found bluetooth device named \"%s\".", device.getName()));
        if (ARCBOTICS_BLUETOOTH_NAME.equals(device.getName())) {
          setBluetoothDevice(device);
        }
      }
    }
  };

  public BluetoothConnector(BluetoothAdapter adapter) {
    mBluetoothAdapter = adapter;
    mConnectLock = false;
    mSendLock = false;
    mRun = false;
  }

  @Override
  public void run() {
    if (mListener != null) {
      mListener.onConnectionStateChange(ConnectionState.DISCONNECTED);
    }
    Log.d(LOG_TAG, "Initializing bluetooth adapter.");
    while (!mBluetoothAdapter.isEnabled()) {
      try {
        Thread.sleep(3000);
      } catch (InterruptedException e) {
        Log.d(LOG_TAG, "Sleep interrupted.", e);
      }
    }
    mBluetoothDevice = getPairedBluetoothDevice();
    if (mBluetoothDevice == null) {
      discoverBluetoothDevice();
    }
    while (mBluetoothDevice == null) {
      Log.d(LOG_TAG, "Waiting for bluetooth device.");
      try {
        Thread.sleep(1000);
      } catch (InterruptedException e) {
        Log.d(LOG_TAG, "Sleep interrupted.", e);
      }
    }
    mBluetoothSocket = createRfcommSocket();
    new Thread(new Runnable() {
      @Override
      public void run() {
        while(mRun) {
          processSendQueue();
        }
      }}).start();
  }

  /**
   * Sets the internal run variable.  Use this to control Thread execution.
   *
   * @param run set false to stop the internal run loop.
   */
  public void setRun(boolean run) {
    mRun = run;
  }

  public BluetoothAdapter getBluetoothAdapter() {
    return mBluetoothAdapter;
  }

  public BluetoothDevice getBluetoothDevice() {
    return mBluetoothDevice;
  }

  public void setBluetoothDevice(BluetoothDevice device) {
    Log.d(LOG_TAG, String.format("Setting bluetooth device named \"%s\".", device.getName()));
    mBluetoothDevice = device;
  }

  /**
   * Runs Bluetooth discovery and attempts to find a device.
   *
   * @return
   */
  public void discoverBluetoothDevice() {
    new Thread(new Runnable() {
      @Override
      public void run() {
        Log.d(LOG_TAG, "No Bluetooth device found.  Starting discovery.");
        if (mBluetoothAdapter.isDiscovering()) {
          Log.d(LOG_TAG, "Bluetooth adapter is already discovering.  Killing it.");
          mBluetoothAdapter.cancelDiscovery();
        }
        mBluetoothAdapter.startDiscovery();
      }}).start();
  }

  /**
   * Adds the provided string to the send queue.
   *
   * @param data the data to send.
   * @throws InterruptedException if the data could not be added to the send queue.
   */
  public void send(String data) throws InterruptedException {
    mSendQueue.offer(data, 3, TimeUnit.SECONDS);
  }

  /**
   * Returns the current bluetooth socket.
   *
   * @return the current bluetooth socket.  May be null.
   */
  public BluetoothSocket getBluetoothSocket() {
    return mBluetoothSocket;
  }

  public boolean isConnected() {
    if (mBluetoothSocket != null) {
      return mBluetoothSocket.isConnected();
    }
    return false;
  }

  public boolean isDevicePaired() {
    return !(mBluetoothDevice == null);
  }

  /**
   * Connects to the bluetooth device.  This method's operations are performed in a separate
   * thread, so it won't block while waiting for the connection.
   */
  public void connect() {
    new Thread(new Runnable() {
      @Override
      public void run() {
        if (mConnectLock) {
          return;
        }
        mConnectLock = true;
        if (mListener != null) {
          mListener.onConnectionStateChange(ConnectionState.CONNECTING);
        }
        mBluetoothSocket = createRfcommSocket();
        connectSocket();
        if (mListener != null) {
          if (isConnected()) {
            mListener.onConnectionStateChange(ConnectionState.CONNECTED);
          } else {
            mListener.onConnectionStateChange(ConnectionState.DISCONNECTED);
          }
        }
        mConnectLock = false;
      }}).start();
  }

  /**
   * Disconnects from the bluetooth device.
   */
  public void disconnect() {
    closeSocket();
    if (mListener != null) {
      mListener.onConnectionStateChange(ConnectionState.DISCONNECTED);
    }
  }

  public void registerListener(ConnectionStateChangeListener listener) {
    mListener = listener;
  }

  /**
   * Connects the bluetooth socket.
   */
  private void connectSocket() {
    Log.d(LOG_TAG, "Connecting bluetooth socket.");
    if (mBluetoothSocket != null && mBluetoothSocket.isConnected()) {
      return;
    }
    while (mBluetoothSocket == null) {
      try {
        Thread.sleep(3000);
      } catch (InterruptedException e) {
        Log.d(LOG_TAG, "Sleep interrupted.", e);
      }
    }
    try {
      mBluetoothAdapter.cancelDiscovery();
      mBluetoothSocket.connect();
    } catch (IOException e) {
      Log.d(
          LOG_TAG,
          "IOException while connecting bluetooth socket.  Device probably isn't available.",
          e);
    } catch (NullPointerException e) {
      Log.d(LOG_TAG, "NullPointerException while connecting bluetooth socket.", e);
    }
    if (mBluetoothSocket.isConnected()) {
      Log.d(LOG_TAG, "Bluetooth socket connected.");
    } else {
      Log.e(LOG_TAG, "Bluetooth socket connection failed.");
    }
  }

  /**
   * Closes the bluetooth socket.
   */
  private void closeSocket() {
    Log.d(LOG_TAG, "Closing bluetooth socket.");
    if (mBluetoothSocket == null && !mBluetoothSocket.isConnected()) {
      return;
    }
    try {
      mBluetoothSocket.close();
    } catch (IOException e) {
      Log.d(
          LOG_TAG,
          "IOException while closing bluetooth socket.  Device probably isn't available.");
    }
  }

  /**
   * Returns the first bluetooth device whose name contains "ArcBotics".
   *
   * @return a BluetoothDevice whose name contains "ArcBotics" or null if not found.
   */
  private BluetoothDevice getPairedBluetoothDevice() {
    Log.d(LOG_TAG, "Getting the bluetooth device.");
    BluetoothDevice device = null;
    if (mBluetoothAdapter != null) {
      Set<BluetoothDevice> pairedDevices = mBluetoothAdapter.getBondedDevices();
      for (BluetoothDevice pairdedDevice : pairedDevices) {
        if (pairdedDevice.getName().contains(ARCBOTICS_BLUETOOTH_NAME)) {
          Log.d(LOG_TAG, "Found ArcBotics bluetooth device.");
          device = pairdedDevice;
          break;
        }
      }
    }
    return device;
  }

  /**
   * Gets an RF Comm socket using the serial port profile UUID.
   *
   * @return an open BluetoothSocket or null.
   */
  private BluetoothSocket createRfcommSocket() {
    Log.d(LOG_TAG, "Creating RF Comm socket.");
    BluetoothSocket socket = null;
    if (mBluetoothDevice != null) {
      try {
        socket = mBluetoothDevice.createRfcommSocketToServiceRecord(SPP_UUID);
      } catch (IOException e) {
        Log.e(LOG_TAG, "IOException raised while getting BluetoothSocket.", e);
      }
    }
    return socket;
  }

  /**
   * Processes the send queue.
   */
  private void processSendQueue() {
    while (mSendLock) {
      try {
        sleep(10);
      } catch (InterruptedException e) {
        Log.d(LOG_TAG, "Sleep interrupted.", e);
      }
    }
    mSendLock = true;
    if (mBluetoothSocket != null && !mSendQueue.isEmpty()) {
      String data = mSendQueue.poll();
      OutputStream outputStream = null;
      try {
        outputStream = mBluetoothSocket.getOutputStream();
      } catch (IOException e) {
        Log.e(LOG_TAG, "IOException while opening output stream.", e);
      }
      try {
        if (!data.endsWith(COMMAND_TERMINATOR)) {
          data = data.concat(COMMAND_TERMINATOR);
        }
        Log.d(LOG_TAG, String.format("Writing data to output stream: \"%s\"", data));
        outputStream.write(data.getBytes());
      } catch (IOException e) {
        Log.e(LOG_TAG, "IOException while sending data.", e);
      }
    }
    mSendLock = false;
    try {
      // Sleep briefly to prevent the process loop from overwhelming the hardware.
      Thread.sleep(3);
    } catch (InterruptedException e) {
      Log.d(LOG_TAG, "Sleep interrupted.", e);
    }
  }
}
